package sttp.tapir.server.metrics

import sttp.monad.MonadError
import sttp.tapir.AnyEndpoint
import sttp.tapir.model.ServerRequest
import sttp.tapir.server.model.ServerResponse

case class Metric[F[_], M](
    metric: M,
    /** Called when the request starts.
      *
      * After this is called, it's guaranteed that exactly one callback sequence on `EndpointMetric` will be invoked:
      *   - `onEndpointRequest` followed by `onResponseHeaders` and `onResponseBody` ("happy path")
      *   - `onEndpointRequest` followed by `onException`
      *   - `onException` (exception in interceptor)
      *   - `onInterceptorResponse`
      *   - `onDecodeFailure`.
      */
    onRequest: (ServerRequest, M, MonadError[F]) => F[EndpointMetric[F]]
)

case class EndpointMetric[F[_]](
    /** Called when an endpoint matches the request, before calling the server logic. */
    onEndpointRequest: Option[AnyEndpoint => F[Unit]] = None,
    /** Called when the response headers are ready (not necessarily the whole response body). */
    onResponseHeaders: Option[(AnyEndpoint, ServerResponse[?]) => F[Unit]] = None,
    /** Called when the response body is complete. */
    onResponseBody: Option[(AnyEndpoint, ServerResponse[?]) => F[Unit]] = None,
    /** Called when an exception is thrown by the server logic or by an interceptor. */
    onException: Option[(AnyEndpoint, Throwable) => F[Unit]] = None,
    /** Called when the response was generated by a request handler in an interceptor, meaning no other metric callbacks associated with the
      * response have been invoked. No endpoint or decode failures are associated with the response in such cases.
      */
    onInterceptorResponse: Option[ServerResponse[?] => F[Unit]] = None,
    /** Called when the request resulted in decode failures for all endpoints, and when no response was generated. No other metric callbacks
      * associated with the request have been invoked.
      */
    onDecodeFailure: Option[() => F[Unit]] = None
) {
  def onEndpointRequest(f: AnyEndpoint => F[Unit]): EndpointMetric[F] = this.copy(onEndpointRequest = Some(f))
  def onResponseHeaders(f: (AnyEndpoint, ServerResponse[?]) => F[Unit]): EndpointMetric[F] = this.copy(onResponseHeaders = Some(f))
  def onResponseBody(f: (AnyEndpoint, ServerResponse[?]) => F[Unit]): EndpointMetric[F] = this.copy(onResponseBody = Some(f))
  def onException(f: (AnyEndpoint, Throwable) => F[Unit]): EndpointMetric[F] = this.copy(onException = Some(f))
  def onInterceptorResponse(f: ServerResponse[?] => F[Unit]): EndpointMetric[F] = this.copy(onInterceptorResponse = Some(f))
  def onDecodeFailure(f: () => F[Unit]): EndpointMetric[F] = this.copy(onDecodeFailure = Some(f))
}

case class ResponsePhaseLabel(name: String, headersValue: String, bodyValue: String)

case class MetricLabels(
    forRequest: List[(String, ServerRequest => String)],
    forEndpoint: List[(String, AnyEndpoint => String)],
    forResponse: List[(String, Either[Throwable, ServerResponse[?]] => Option[String])],
    forResponsePhase: ResponsePhaseLabel = ResponsePhaseLabel("phase", "headers", "body")
) {
  def namesForRequest: List[String] = forRequest.map { case (name, _) => name }
  def namesForEndpoint: List[String] = forEndpoint.map { case (name, _) => name }
  def namesForResponse: List[String] = forResponse.map { case (name, _) => name }

  def valuesForRequest(req: ServerRequest): List[String] = forRequest.map { case (_, f) => f(req) }
  def valuesForEndpoint(ep: AnyEndpoint): List[String] = forEndpoint.map { case (_, f) => f(ep) }
  def valuesForResponse(res: ServerResponse[?]): List[String] = forResponse.flatMap { case (_, f) => f(Right(res)).toList }
  def valuesForResponse(ex: Throwable): List[String] = forResponse.flatMap { case (_, f) => f(Left(ex)).toList }
}

case class MetricLabelsTyped[A](
    forRequest: List[ServerRequest => A],
    forEndpoint: List[AnyEndpoint => A],
    forResponse: List[Either[Throwable, ServerResponse[?]] => Option[A]],
    forResponsePhase: ResponsePhaseLabel = ResponsePhaseLabel("phase", "headers", "body")
)

object MetricLabels {

  /** Labels request by path and method, response by status code */
  lazy val Default: MetricLabels = MetricLabels(
    forRequest = List(
      "method" -> { req => req.method.method }
    ),
    forEndpoint = List(
      "path" -> { ep => ep.showPathTemplate(showQueryParam = None) }
    ),
    forResponse = List(
      "status" -> {
        case Right(r) =>
          Some(r.code match {
            case c if c.isInformational => "1xx"
            case c if c.isSuccess       => "2xx"
            case c if c.isRedirect      => "3xx"
            case c if c.isClientError   => "4xx"
            case c if c.isServerError   => "5xx"
            case _                      => ""
          })
        case Left(_) => Some("5xx")
      }
    )
  )
}
